package server;

import java.io.IOException;
import java.util.HashMap;

import commons.Message;
import commons.Player;
import commons.PlayersRepository;
import commons.OMGProtocol;
import commons.World;

/**
 * 
 * DFA Implementation for Server
 *
 */
public class OMGProtocolForServer extends OMGProtocol{
	PlayersRepository playersRepository;
	String servername;
	private HashMap<Integer, World> games;
	private int clientID;
	private long lastUpdateSent;

	public OMGProtocolForServer(String servername, HashMap<Integer, World> games, int clientID)
	{
		this.lastUpdateSent = 0;
		this.clientID = clientID;
		this.currentState = IDLE;
		this.playersRepository = new PlayersRepository();
		this.servername = servername;
		this.games = games;
	}


	// The main DFA Code, gets called while program is running
	public Message runDFA(Message newMessage) throws IOException, ClassNotFoundException {
		Message message = null;

		if(!newMessage.isRead())
			// A new message has come in
		{
			if(newMessage.getTimeStamp() >= this.lastTimeRemote) {
				message = newMessage;	
				this.lastTimeRemote = message.getTimeStamp();
				this.lastTimeLocal = System.currentTimeMillis();
				this.waitingForResponse = false;
			} else {
				System.err.println("** Got a stale packet, ignoring. **" + newMessage.getTimeStamp());
			}
		}


		switch(currentState){
		case IDLE:
			return handleServerIdleState(message);

		case CONNECTED:
			return handleServerConnectedState(message);

		case JOINING_GAME:
			return handleServerJoiningGameState(message);

		case LIST_REQUEST:
			return handleServerListRequestState(message);

		case PINGING:
			return handleServerPingingState(message);

		case PLAYING_GAME:
			return handleServerPlayingGameState(message);
		default:		
			throw new RuntimeException("Invalid state!");
		}
	}


	// Handles state where server is getting and sending update messages
	private Message handleServerPlayingGameState(Message message) throws IOException, ClassNotFoundException {
		if(message == null) {
			if(games.get(this.gameID).hasChangedSince(this.lastUpdateSent)) {
				System.out.println("World has changed, sending to client " + clientID);
				this.lastUpdateSent = System.currentTimeMillis();
				return new Message(this.servername, this.gameID, Message.GAME_UPDATE, serialize(games.get(this.gameID)));
			}
		} else {
			switch(message.getMessageCode()) {
			case Message.GAME_UPDATE:
				// Indicate message was read
				message.read();

				Player p = (Player) deserialize(message.getPayload());
				games.get(this.gameID).updatePlayer(this.clientID, p);

				System.err.println("[" + clientID + "] Got player update");

				return null;
			case Message.PING:
				this.currentState = PINGING;
				return null;
			case Message.DISCONNECT:
				message.read();
				games.get(this.gameID).removePlayer(clientID);
				this.currentState = NONE;
				return null;
			default: 
				// Ignore other message types
				message.read();
			return null;
			}
		}


		// Check when last timestamp was
		long timeLapse = System.currentTimeMillis() - this.lastTimeLocal;
		if(timeLapse > TIME_OUT && !waitingForResponse) {
			this.waitingForResponse = true;
			this.currentState = PINGING;
			return new Message(this.servername, Message.PING);	
		}

		return null;
	}

	// Handles state where server waits or sends a PONG message
	private Message handleServerPingingState(Message message) {
		if(message != null) {
			// Indicate message has been read
			message.read();

			switch(message.getMessageCode()) {
			case Message.PONG:
				this.currentState = PLAYING_GAME;
				return null;
			default:
				return null;			
			}
		} else if(!this.waitingForResponse) {
			// Send a pong message
			this.currentState = PLAYING_GAME;
			return new Message(this.servername, Message.PONG);
		} else {
			long timeLapse = System.currentTimeMillis() - this.lastTimeLocal;
			if(timeLapse > (PING_MAX*TIME_OUT)) {
				this.currentState = NONE;
				System.err.println("[" + clientID + "] Client timed out");
				games.get(this.gameID).removePlayer(clientID);
			}
		}
		return null;

	}


	// Handles state where server has a list request message to respond to
	private Message handleServerListRequestState(Message message) {
		if(message == null) {			
			// Go back to connected state
			currentState = CONNECTED;
			return new Message(servername,
					games);
		}		

		// It does not make sense for the server to receive messages in this state
		message.read();
		return null;
	}

	// Handles state where server is checking if the client can join a game
	private Message handleServerJoiningGameState(Message message) {
		if(message != null) {
			// Indicate message was read
			message.read();

			// Accept connections to games that have even numbered ids only
			if(games.containsKey(new Integer(message.getGameId()))) {
				this.gameID = new Integer(message.getGameId());
				this.currentState = PLAYING_GAME;
				return new Message(this.servername, this.gameID, Message.JOIN_ACK, message.getUsername());
			} else {
				this.currentState = CONNECTED;
				return new Message(this.servername, new Integer(message.getGameId()), Message.JOIN_FAIL, message.getUsername());
			}
		}
		return null;
	}


	// Handles state where client has connected and can request game list,
	// or try to join a game
	private Message handleServerConnectedState(Message message) {

		if(message != null) {
			switch(message.getMessageCode()) {
			case Message.LIST_REQUEST:
				// Indicate message was read
				message.read();

				this.currentState = LIST_REQUEST;
				return null;

			case Message.JOIN_GAME:
				// Message was not read yet

				currentState = JOINING_GAME;
				return null;
			default:
				// Ignore
				message.read();
			return null;			
			}
		}

		return null;
	}


	// Handles default server state
	private Message handleServerIdleState(Message message) 
	{
		if(message != null) {
			// Indicate message was read
			message.read();

			switch(message.getMessageCode()) {
			case Message.CONNECT:

				String username = message.getUsername();
				String password = message.getPassword();
				String servername = message.getServerName();


				// Indicate message was read
				message.read();

				// Check username and password
				if(playersRepository.getPassword(username) != null && 
						playersRepository.getPassword(username).equalsIgnoreCase(password))
				{
					// We are now waiting for a client message
					currentState = CONNECTED;
					return new Message(servername, 0 ,Message.CONNECT_OK, username);
				}
				else
				{
					// Drop connection
					currentState = NONE;
					return new Message(servername, 0, Message.BAD_AUTH, username);
				}
			default: 
				// Ignore other messages
				message.read();
			return null;
			}
		}
		return null;
	}

}
